﻿using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using XamlKru.Controls.Primitives;
using XamlKru.Math3D;

namespace SilverlightApplication.UIControls.Carousel
{
    public class Plane3D : Canvas
    {
        #region Members

        double Width3DV;
        double Height3DV;
        BitmapImage BITexture;

        private Matrix3D _projectionMatrix;

        private PlaneModel3D _planeModel;

        private int divisionCount = 2;

        DispatcherTimer dt;
        private MouseEvent lastEvent = MouseEvent.Out;

        #endregion

        #region Events

        public delegate void PlaneClick();
        public event PlaneClick planeClick;
        public delegate void PlaneEnter();
        public event PlaneEnter planeEnter;
        public delegate void PlaneLeave();
        public event PlaneLeave planeLeave;

        #endregion

        #region Dependency Properties

        public static readonly DependencyProperty XProperty;
        public static readonly DependencyProperty YProperty;
        public static readonly DependencyProperty ScaleProperty;
        public static readonly DependencyProperty PitchProperty;
        public static readonly DependencyProperty YawProperty;
        public static readonly DependencyProperty RollProperty;

        #endregion

        #region Properties

        public double X
        {
            get { return (double)GetValue(XProperty); }
            set { SetValue(XProperty, value); }
        }

        public double Y
        {
            get { return (double)GetValue(YProperty); }
            set { SetValue(YProperty, value); }
        }

        public double Scale
        {
            get { return (double)GetValue(ScaleProperty); }
            set { SetValue(ScaleProperty, value); }
        }

        public double Pitch
        {
            get { return (double)GetValue(PitchProperty); }
            set { SetValue(PitchProperty, value); }
        }

        public double Yaw
        {
            get { return (double)GetValue(YawProperty); }
            set { SetValue(YawProperty, value); }
        }

        public double Roll
        {
            get { return (double)GetValue(RollProperty); }
            set { SetValue(RollProperty, value); }
        }

        #endregion

        #region Constructors

        static Plane3D()
        {
            XProperty = DependencyProperty.Register("X",
                                                    typeof(double),
                                                    typeof(Plane3D),
                                                    new PropertyMetadata(OnXChanged));

            YProperty = DependencyProperty.Register("Y",
                                                    typeof(double),
                                                    typeof(Plane3D),
                                                    new PropertyMetadata(OnYChanged));

            ScaleProperty = DependencyProperty.Register("Scale",
                                                    typeof(double),
                                                    typeof(Plane3D),
                                                    new PropertyMetadata(OnScaleChanged));

            PitchProperty = DependencyProperty.Register("Pitch",
                                                    typeof(double),
                                                    typeof(Plane3D),
                                                    new PropertyMetadata(OnPitchChanged));

            YawProperty = DependencyProperty.Register("Yaw",
                                                    typeof(double),
                                                    typeof(Plane3D),
                                                    new PropertyMetadata(OnYawChanged));

            RollProperty = DependencyProperty.Register("Roll",
                                                    typeof(double),
                                                    typeof(Plane3D),
                                                    new PropertyMetadata(OnRollChanged));
        }

        public Plane3D()
        {
            // Timer for planeEnter and planeLeave handling
            dt = new DispatcherTimer();
            dt.Interval = new TimeSpan(0);
            dt.Tick += new EventHandler(dt_Tick);

            Loaded += new RoutedEventHandler(Plane3D_Loaded);
            SizeChanged += new SizeChangedEventHandler(Plane3D_SizeChanged);
            _planeModel = new PlaneModel3D(divisionCount, divisionCount);
            CreateProjectionMatrix();

            AspectRatio = 4 / 3.0f;
        }

        public Plane3D(int d)
        {
            // Timer for planeEnter and planeLeave handling
            dt = new DispatcherTimer();
            dt.Interval = new TimeSpan(0);
            dt.Tick += new EventHandler(dt_Tick);

            this.divisionCount = d;
            Loaded += new RoutedEventHandler(Plane3D_Loaded);
            SizeChanged += new SizeChangedEventHandler(Plane3D_SizeChanged);
            _planeModel = new PlaneModel3D(divisionCount, divisionCount);
            CreateProjectionMatrix();

            AspectRatio = 4 / 3.0f;
        }

        #endregion

        #region Implementation

        private static void OnXChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            Plane3D p = (Plane3D)obj;

            p.CreateProjectionMatrix();
            p.UpdateScene();
        }

        private static void OnYChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            Plane3D p = (Plane3D)obj;

            p.CreateProjectionMatrix();
            p.UpdateScene();
        }

        private static void OnScaleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            Plane3D p = (Plane3D)obj;

            p.CreateProjectionMatrix();
            p.UpdateScene();
        }

        private static void OnPitchChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            Plane3D p = (Plane3D)obj;
            p.UpdateScene();
        }

        private static void OnYawChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            Plane3D p = (Plane3D)obj;
            p.UpdateScene();
        }

        private static void OnRollChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            Plane3D p = (Plane3D)obj;
            p.UpdateScene();
        }

        private void CreateProjectionMatrix()
        {
            Double fov = 45 * Math.PI / 180d;

            Matrix3D prj = new Matrix3D
            {
                M11 = 1d / Math.Tan(fov * .5),
                M22 = 1d / Math.Tan(fov * .5),
                M33 = 1d,
                M34 = 1d,
                OffsetZ = -1d
            };

            double Zoom = 0.0044;
            double ViewportAspectRatio = 6 / 13d;
            double a = Width3DV * ViewportAspectRatio * Zoom;
            double b = Height3DV * Zoom;

            // Proportional Scaling
            if (a / b > 1)
                a = b;
            else
                b = a;

            double x, y;

            AspectRatio = 4 / 3.0f;

            if (150 * AspectRatio <= 200)
            {
                x = 150 * AspectRatio * a;
                y = 150 * b;
            }
            else
            {
                x = 200 * a;
                y = 150 * b / AspectRatio;
            }

            _projectionMatrix = Matrix3D.Identity
                                        .Scale(new Vector3D(Scale, Scale, 1))
                                        .Translate(new Vector3D(0, 0, 5))
                                        * prj
                                        .Scale(new Vector3D(x, y, 1))
                                        .Translate(new Vector3D(Width3DV / 2, Height3DV / 2, 0))
                                        .Translate(new Vector3D(X * a, Y * b, 0));
        }

        private void UpdatePlane()
        {
            this.Children.Clear();

            Int32 count = (Int32)(_planeModel.Indices.Count / 3);

            for (Int32 i = 0; i < count; i++)
            {
                var tri = new ImageTriangle
                {
                    TexturePositions = _planeModel.GetTexturePositions(i),
                    TextureSource = BITexture,
                    Cursor = Cursors.Hand
                };
                
                this.Children.Add(tri);

                if (planeLeave != null)
                    tri.MouseLeftButtonUp += new MouseButtonEventHandler(tri_MouseLeftButtonUp);
                if (planeEnter != null)
                    tri.MouseEnter += new MouseEventHandler(tri_MouseEnter);
                if (planeLeave != null)
                    tri.MouseLeave += new MouseEventHandler(tri_MouseLeave);
            }
        }

        void tri_MouseLeave(object sender, MouseEventArgs e)
        {
            lastEvent = MouseEvent.Leave;
            dt.Start();
        }

        void dt_Tick(object sender, EventArgs e)
        {
            dt.Stop();
            if (lastEvent == MouseEvent.Leave)
            {
                if (planeLeave != null)
                    planeLeave();
                lastEvent = MouseEvent.Out;
            }
        }

        void tri_MouseEnter(object sender, MouseEventArgs e)
        {
            if (lastEvent == MouseEvent.Out)
            {
                if (planeEnter != null)
                    planeEnter();
            }
            lastEvent = MouseEvent.Enter;
        }

        void tri_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (planeClick != null)
                planeClick();
        }

        private void UpdateScene()
        {
            // create matrix
            var mtx = (Quaternion.ForRotation(new Vector3D(0, 1, 0), Pitch)
                        * Quaternion.ForRotation(new Vector3D(-1, 0, 0), Yaw)
                        * Quaternion.ForRotation(new Vector3D(0, 0, 1), Roll)
                      )
                      .ToMatrix()
                      * _projectionMatrix;

            // project vertices
            var points = (_planeModel.Vertices * mtx)   // apply matrix
                          .Select(p => p.ToPoint())     // to 2D
                          .ToArray();                   // to Array

            Int32 i = 0;
            foreach (ImageTriangle tri in this.Children)
            {
                // find triangle vertices by index
                Int32 a = _planeModel.Indices[i * 3],
                      b = _planeModel.Indices[i * 3 + 1],
                      c = _planeModel.Indices[i * 3 + 2];

                tri.SetPoints(points[a], points[b], points[c]);

                i++;
            }
        }

        void Plane3D_Loaded(object sender, RoutedEventArgs e)
        {
            UpdatePlane();
            UpdateScene();
        }

        private void Plane3D_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            Width3DV = e.NewSize.Width;
            Height3DV = e.NewSize.Height;
            CreateProjectionMatrix();
            UpdateScene();
        }

        #endregion

        #region Public Methods

        public void setRotation(double pitch, double yaw, double roll)
        {
            Pitch = pitch;
            Yaw = yaw;
            Roll = Roll;

            UpdateScene();
        }

        public void setPosition(double x, double y)
        {
            X = x;
            Y = y;

            CreateProjectionMatrix();
            UpdateScene();
        }

        public void setImage(BitmapImage b)
        {
            BITexture = b;
            UpdatePlane();
        }

        public BitmapImage getImage()
        {
            return BITexture;
        }

        public double AspectRatio { get; set; }

        #endregion
    }

    enum MouseEvent
    {
        Enter, Leave, Out
    }
}
